oak-backend-base 3.3.3 → 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.
@@ -1,471 +1,530 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- const relationPath_1 = require("oak-domain/lib/utils/relationPath");
5
- const assert_1 = tslib_1.__importDefault(require("assert"));
6
- const path_1 = require("path");
7
- const lodash_1 = require("oak-domain/lib/utils/lodash");
8
- const filter_1 = require("oak-domain/lib/store/filter");
9
- const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
10
- const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
11
- class Synchronizer {
12
- config;
13
- schema;
14
- selfEncryptInfo;
15
- remotePullInfoMap = {};
16
- pullMaxBornAtMap = {};
17
- remotePushChannel = {};
18
- /**
19
- * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
20
- * @param channel
21
- * @param retry
22
- */
23
- async pushOnChannel(remoteEntity, remoteEntityId, context, channel, retry) {
24
- const { queue, api, nextPushTimestamp } = channel;
25
- (0, assert_1.default)(nextPushTimestamp);
26
- // 失败重试的间隔,失败次数多了应当适当延长,最多延长到1024秒
27
- let nextPushTimestamp2 = typeof retry === 'number' ? Math.pow(2, Math.min(retry, 10)) : 1;
28
- channel.nextPushTimestamp = nextPushTimestamp2 * 1000 + Date.now();
29
- const opers = queue.map(ele => ele.oper);
30
- let restOpers = [];
31
- let needRetry = false;
32
- let json;
33
- try {
34
- // todo 加密
35
- const selfEncryptInfo = await this.getSelfEncryptInfo(context);
36
- console.log('向远端结点sync数据', api, JSON.stringify(opers));
37
- const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
38
- const res = await fetch(finalApi, {
39
- method: 'post',
40
- headers: {
41
- 'Content-Type': 'application/json',
42
- [OAK_SYNC_HEADER_ENTITY]: remoteEntity,
43
- [OAK_SYNC_HEADER_ENTITYID]: remoteEntityId,
44
- },
45
- body: JSON.stringify(opers),
46
- });
47
- if (res.status !== 200) {
48
- throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
49
- }
50
- json = await res.json();
51
- }
52
- catch (err) {
53
- console.error('sync push时出现error', err);
54
- needRetry = true;
55
- restOpers = queue;
56
- }
57
- if (!needRetry) {
58
- /**
59
- * 返回结构见this.getSelfEndpoint
60
- */
61
- const { successIds, failed } = json;
62
- if (failed) {
63
- needRetry = true;
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();
70
- }
71
- else {
72
- restOpers.push(req);
73
- }
74
- }
75
- }
76
- if (restOpers.length > 0) {
77
- const interval = Math.max(0, channel.nextPushTimestamp - Date.now());
78
- const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined;
79
- console.log('need retry', retry2);
80
- setTimeout(() => this.pushOnChannel(remoteEntity, remoteEntityId, context, channel, retry2), interval);
81
- }
82
- else {
83
- channel.handler = undefined;
84
- channel.nextPushTimestamp = undefined;
85
- }
86
- }
87
- // 将产生的oper推送到远端Node。注意要尽量在本地阻止重复推送
88
- /**
89
- * 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
90
- * 因此在这里要实现两点:
91
- * 1)oper如果推送失败了,必须留存在queue中,以保证在后面产生的oper之前推送
92
- * 2)当对queue中增加oper时,要检查是否有重(有重说明之前失败过),如果无重则将之放置在队列尾
93
- *
94
- * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
95
- */
96
- async pushOper(context, oper, userId, url, endpoint, remoteEntity, remoteEntityId, nextPushTimestamp) {
97
- if (!this.remotePushChannel[userId]) {
98
- this.remotePushChannel[userId] = {
99
- api: (0, path_1.join)(url, 'endpoint', endpoint),
100
- queue: [],
101
- };
102
- }
103
- const channel = this.remotePushChannel[userId];
104
- // 要去重且有序
105
- let existed = false;
106
- let idx = 0;
107
- for (; idx < channel.queue.length; idx++) {
108
- if (channel.queue[idx].oper.id === oper.id) {
109
- existed = true;
110
- break;
111
- }
112
- else if (channel.queue[idx].oper.bornAt > oper.bornAt) {
113
- break;
114
- }
115
- }
116
- if (!existed) {
117
- const now = Date.now();
118
- const nextPushTimestamp2 = nextPushTimestamp || now + 1000;
119
- const waiter = new Promise((resolve, reject) => {
120
- if (!existed) {
121
- channel.queue.splice(idx, 0, {
122
- oper,
123
- resolve,
124
- reject,
125
- });
126
- }
127
- });
128
- if (!channel.handler) {
129
- channel.nextPushTimestamp = nextPushTimestamp2;
130
- channel.handler = setTimeout(async () => {
131
- await this.pushOnChannel(remoteEntity, remoteEntityId, context, channel);
132
- }, nextPushTimestamp2 - now);
133
- }
134
- else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) {
135
- // 当前队列的开始时间要晚于自身的要求,要求提前开始
136
- channel.nextPushTimestamp = nextPushTimestamp2;
137
- }
138
- await waiter;
139
- }
140
- else {
141
- // 感觉应该跑不出来
142
- console.warn('在sync数据时,遇到了重复推送的oper', JSON.stringify(oper), userId, url);
143
- }
144
- }
145
- async getSelfEncryptInfo(context) {
146
- if (this.selfEncryptInfo) {
147
- return this.selfEncryptInfo;
148
- }
149
- this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
150
- return this.selfEncryptInfo;
151
- }
152
- makeCreateOperTrigger() {
153
- const { config } = this;
154
- const { remotes, self } = config;
155
- // 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
156
- const pushAccessMap = {};
157
- remotes.forEach((remote) => {
158
- const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
159
- if (pushEntityDefs) {
160
- const pushEntities = [];
161
- const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
162
- for (const def of pushEntityDefs) {
163
- const { path, relationName, recursive, entity, actions, onSynchronized } = def;
164
- pushEntities.push(entity);
165
- const relationName2 = relationName || rnRemote;
166
- const path2 = pathToUser ? `${path}.${pathToUser}` : path;
167
- const { projection, getData } = relationName2 ? (0, relationPath_1.destructRelationPath)(this.schema, entity, path2, {
168
- relation: {
169
- name: relationName,
170
- }
171
- }, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
172
- const groupByUsers = (rows) => {
173
- const userRowDict = {};
174
- rows.forEach((row) => {
175
- const goals = getData(row);
176
- if (goals) {
177
- goals.forEach(({ entity, entityId, userId }) => {
178
- if (userRowDict[userId]) {
179
- // 逻辑上来说同一个userId,其关联的entity和entityId必然相同,这个entity/entityId代表了对方
180
- (0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
181
- userRowDict[userId].rowIds.push(row.id);
182
- }
183
- else {
184
- userRowDict[userId] = {
185
- entity,
186
- entityId,
187
- rowIds: [row.id],
188
- };
189
- }
190
- });
191
- }
192
- });
193
- return userRowDict;
194
- };
195
- if (!pushAccessMap[entity]) {
196
- pushAccessMap[entity] = [{
197
- projection,
198
- groupByUsers,
199
- getRemotePushInfo: getPushInfo,
200
- endpoint: endpoint2,
201
- entity,
202
- actions,
203
- onSynchronized
204
- }];
205
- }
206
- else {
207
- pushAccessMap[entity].push({
208
- projection,
209
- groupByUsers,
210
- getRemotePushInfo: getPushInfo,
211
- endpoint: endpoint2,
212
- entity,
213
- actions,
214
- onSynchronized
215
- });
216
- }
217
- }
218
- }
219
- });
220
- const pushEntities = Object.keys(pushAccessMap);
221
- // push相关联的entity,在发生操作时,需要将operation推送到远端
222
- const createOperTrigger = {
223
- name: 'push oper to remote node',
224
- entity: 'oper',
225
- action: 'create',
226
- when: 'commit',
227
- strict: 'makeSure',
228
- check: (operation) => {
229
- const { data } = operation;
230
- return pushEntities.includes(data.targetEntity);
231
- },
232
- fn: async ({ ids }, context) => {
233
- (0, assert_1.default)(ids.length === 1);
234
- const [oper] = await context.select('oper', {
235
- data: {
236
- id: 1,
237
- action: 1,
238
- data: 1,
239
- targetEntity: 1,
240
- operatorId: 1,
241
- operEntity$oper: {
242
- $entity: 'operEntity',
243
- data: {
244
- id: 1,
245
- entity: 1,
246
- entityId: 1,
247
- },
248
- },
249
- bornAt: 1,
250
- $$createAt$$: 1,
251
- },
252
- filter: {
253
- id: ids[0],
254
- }
255
- }, { dontCollect: true, forUpdate: true });
256
- const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
257
- const entityIds = operEntities.map(ele => ele.entityId);
258
- const pushEntityNodes = pushAccessMap[targetEntity];
259
- if (pushEntityNodes && pushEntityNodes.length > 0) {
260
- // 每个pushEntityNode代表配置的一个remoteEntity
261
- await Promise.all(pushEntityNodes.map(async (node) => {
262
- const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
263
- if (!actions || actions.includes(action)) {
264
- const pushed = [];
265
- const rows = await context.select(targetEntity, {
266
- data: {
267
- id: 1,
268
- ...projection,
269
- },
270
- filter: {
271
- id: {
272
- $in: entityIds,
273
- },
274
- },
275
- }, { dontCollect: true, includedDeleted: true });
276
- // userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
277
- const userSendDict = groupByUsers(rows);
278
- const pushToUserIdFn = async (userId) => {
279
- const { entity, entityId, rowIds } = userSendDict[userId];
280
- // 推送到远端结点的oper
281
- const oper2 = {
282
- id: oper.id,
283
- action: action,
284
- data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
285
- filter: {
286
- id: rowIds.length === 1 ? rowIds[0] : {
287
- $in: rowIds,
288
- }
289
- },
290
- bornAt: oper.bornAt,
291
- targetEntity,
292
- };
293
- const { url } = await getRemoteAccessInfo(context, {
294
- userId,
295
- remoteEntityId: entityId,
296
- });
297
- await this.pushOper(context, oper2 /** 这里不明白为什么TS过不去 */, userId, url, endpoint, entity, entityId);
298
- };
299
- for (const userId in userSendDict) {
300
- if (userId !== operatorId) {
301
- pushed.push(pushToUserIdFn(userId));
302
- }
303
- }
304
- if (pushed.length > 0) {
305
- await Promise.all(pushed);
306
- if (onSynchronized) {
307
- await onSynchronized({
308
- action: action,
309
- data: data,
310
- rowIds: entityIds,
311
- }, context);
312
- }
313
- }
314
- }
315
- }));
316
- return entityIds.length * pushEntityNodes.length;
317
- }
318
- return 0;
319
- }
320
- };
321
- return createOperTrigger;
322
- }
323
- constructor(config, schema) {
324
- this.config = config;
325
- this.schema = schema;
326
- }
327
- /**
328
- * 根据sync的定义,生成对应的 commit triggers
329
- * @returns
330
- */
331
- getSyncTriggers() {
332
- return [this.makeCreateOperTrigger()];
333
- }
334
- getSelfEndpoint() {
335
- return {
336
- name: this.config.self.endpoint || 'sync',
337
- method: 'post',
338
- params: ['entity', 'entityId'],
339
- fn: async (context, params, headers, req, body) => {
340
- // body中是传过来的oper数组信息
341
- const { entity, entityId } = params;
342
- const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
343
- console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
344
- const successIds = [];
345
- let failed;
346
- // todo 这里先缓存,不考虑本身同步相关信息的更新
347
- if (!this.remotePullInfoMap[entity]) {
348
- this.remotePullInfoMap[entity] = {};
349
- }
350
- if (!this.remotePullInfoMap[entity][entityId]) {
351
- const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
352
- const pullEntityDict = {};
353
- if (pullEntities) {
354
- pullEntities.forEach((def) => pullEntityDict[def.entity] = def);
355
- }
356
- this.remotePullInfoMap[entity][entityId] = {
357
- pullInfo: await getPullInfo(context, {
358
- selfId: meEntityId,
359
- remoteEntityId: entityId,
360
- }),
361
- pullEntityDict,
362
- };
363
- }
364
- const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId];
365
- const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
366
- (0, assert_1.default)(userId);
367
- context.setCurrentUserId(userId);
368
- if (cxtInfo) {
369
- await context.initialize(cxtInfo);
370
- }
371
- const selfEncryptInfo = await this.getSelfEncryptInfo(context);
372
- (0, assert_1.default)(selfEncryptInfo.id === meEntityId && meEntity === this.config.self.entity);
373
- // todo 解密
374
- if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
375
- const [maxHisOper] = await context.select('oper', {
376
- data: {
377
- id: 1,
378
- bornAt: 1,
379
- },
380
- filter: {
381
- operatorId: userId,
382
- },
383
- sorter: [
384
- {
385
- $attr: {
386
- bornAt: 1,
387
- },
388
- $direction: 'desc',
389
- },
390
- ],
391
- indexFrom: 0,
392
- count: 1,
393
- }, { dontCollect: true });
394
- this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt || 0;
395
- }
396
- let maxBornAt = this.pullMaxBornAtMap[entityId];
397
- const opers = body;
398
- const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
399
- const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
400
- await Promise.all([
401
- // 无法严格保证推送按bornAt,所以一旦还有outdatedOpers,检查其已经被apply
402
- (async () => {
403
- const ids = outdatedOpers.map(ele => ele.id);
404
- if (ids.length > 0) {
405
- const opersExisted = await context.select('oper', {
406
- data: {
407
- id: 1,
408
- },
409
- filter: {
410
- id: {
411
- $in: ids,
412
- }
413
- }
414
- }, { dontCollect: true });
415
- if (opersExisted.length < ids.length) {
416
- const missed = (0, lodash_1.difference)(ids, opersExisted.map(ele => ele.id));
417
- // todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
418
- throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
419
- }
420
- successIds.push(...ids);
421
- }
422
- })(),
423
- (async () => {
424
- for (const freshOper of freshOpers) {
425
- // freshOpers是按bornAt序产生的
426
- const { id, targetEntity, action, data, bornAt, filter } = freshOper;
427
- const ids = (0, filter_1.getRelevantIds)(filter);
428
- (0, assert_1.default)(ids.length > 0);
429
- try {
430
- if (pullEntityDict && pullEntityDict[targetEntity]) {
431
- const { process } = pullEntityDict[targetEntity];
432
- if (process) {
433
- await process(action, data, context);
434
- }
435
- }
436
- const operation = {
437
- id,
438
- data,
439
- action,
440
- filter: {
441
- id: ids.length === 1 ? ids[0] : {
442
- $in: ids,
443
- },
444
- },
445
- bornAt: bornAt,
446
- };
447
- await context.operate(targetEntity, operation, {});
448
- successIds.push(id);
449
- maxBornAt = bornAt;
450
- }
451
- catch (err) {
452
- console.error('sync时出错', entity, JSON.stringify(freshOper));
453
- failed = {
454
- id,
455
- error: err.toString(),
456
- };
457
- break;
458
- }
459
- }
460
- })()
461
- ]);
462
- this.pullMaxBornAtMap[entityId] = maxBornAt;
463
- return {
464
- successIds,
465
- failed,
466
- };
467
- }
468
- };
469
- }
470
- }
471
- exports.default = Synchronizer;
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const types_1 = require("oak-domain/lib/types");
5
+ const relationPath_1 = require("oak-domain/lib/utils/relationPath");
6
+ const assert_1 = tslib_1.__importDefault(require("assert"));
7
+ const path_1 = require("path");
8
+ const lodash_1 = require("oak-domain/lib/utils/lodash");
9
+ const filter_1 = require("oak-domain/lib/store/filter");
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';
13
+ class Synchronizer {
14
+ config;
15
+ schema;
16
+ remotePullInfoMap = {};
17
+ pullMaxBornAtMap = {};
18
+ remotePushChannel = {};
19
+ pushAccessMap = {};
20
+ /**
21
+ * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
22
+ * @param channel
23
+ * @param retry
24
+ */
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;
30
+ const opers = queue.map(ele => ele.oper);
31
+ let failedOpers = [];
32
+ let needRetry = false;
33
+ let json;
34
+ try {
35
+ // todo 加密
36
+ console.log('向远端结点sync数据', api, JSON.stringify(opers));
37
+ const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
38
+ const res = await fetch(finalApi, {
39
+ method: 'post',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ [OAK_SYNC_HEADER_ENTITY]: entity,
43
+ [OAK_SYNC_HEADER_ENTITYID]: entityId,
44
+ },
45
+ body: JSON.stringify(opers),
46
+ });
47
+ if (res.status !== 200) {
48
+ throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
49
+ }
50
+ json = await res.json();
51
+ }
52
+ catch (err) {
53
+ console.error('sync push时出现error', err);
54
+ needRetry = true;
55
+ failedOpers = queue;
56
+ }
57
+ if (!needRetry) {
58
+ /**
59
+ * 返回结构见this.getSelfEndpoint
60
+ */
61
+ const { successIds, failed } = json;
62
+ if (failed) {
63
+ needRetry = true;
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
+ }
75
+ }
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
+ }
121
+ }
122
+ else {
123
+ channel.handler = undefined;
124
+ channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
125
+ }
126
+ }
127
+ // 将产生的oper推送到远端Node。注意要尽量在本地阻止重复推送
128
+ /**
129
+ * 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
130
+ * 因此在这里要实现两点:
131
+ * 1)oper如果推送失败了,必须留存在queue中,以保证在后面产生的oper之前推送
132
+ * 2)当对queue中增加oper时,要检查是否有重(有重说明之前失败过),如果无重则将之放置在队列尾
133
+ *
134
+ * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
135
+ */
136
+ async pushOper(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo) {
137
+ if (!this.remotePushChannel[userId]) {
138
+ // channel上缓存这些信息,暂不支持动态更新
139
+ this.remotePushChannel[userId] = {
140
+ api: (0, path_1.join)(url, 'endpoint', endpoint),
141
+ queue: [],
142
+ entity: remoteEntity,
143
+ entityId: remoteEntityId,
144
+ nextPushTimestamp: Number.MAX_SAFE_INTEGER,
145
+ running: false,
146
+ selfEncryptInfo,
147
+ };
148
+ }
149
+ const channel = this.remotePushChannel[userId];
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,
191
+ }
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
+ }
252
+ }
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
+ }, {});
267
+ }
268
+ return 0;
269
+ }
270
+ makeCreateOperTrigger() {
271
+ const { config } = this;
272
+ const { remotes, self } = config;
273
+ // 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
274
+ remotes.forEach((remote) => {
275
+ const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
276
+ if (pushEntityDefs) {
277
+ const pushEntities = [];
278
+ const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
279
+ for (const def of pushEntityDefs) {
280
+ const { path, relationName, recursive, entity, actions, onSynchronized } = def;
281
+ pushEntities.push(entity);
282
+ const relationName2 = relationName || rnRemote;
283
+ const path2 = pathToUser ? `${path}.${pathToUser}` : path;
284
+ const { projection, getData } = relationName2 ? (0, relationPath_1.destructRelationPath)(this.schema, entity, path2, {
285
+ relation: {
286
+ name: relationName,
287
+ }
288
+ }, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
289
+ const groupByUsers = (rows) => {
290
+ const userRowDict = {};
291
+ rows.forEach((row) => {
292
+ const goals = getData(row);
293
+ if (goals) {
294
+ goals.forEach(({ entity, entityId, userId }) => {
295
+ if (userRowDict[userId]) {
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);
299
+ }
300
+ else {
301
+ userRowDict[userId] = {
302
+ entity,
303
+ entityId,
304
+ rowIds: [row.id],
305
+ };
306
+ }
307
+ });
308
+ }
309
+ });
310
+ return userRowDict;
311
+ };
312
+ if (!this.pushAccessMap[entity]) {
313
+ this.pushAccessMap[entity] = [{
314
+ projection,
315
+ groupByUsers,
316
+ getRemotePushInfo: getPushInfo,
317
+ endpoint: endpoint2,
318
+ entity,
319
+ actions,
320
+ onSynchronized
321
+ }];
322
+ }
323
+ else {
324
+ this.pushAccessMap[entity].push({
325
+ projection,
326
+ groupByUsers,
327
+ getRemotePushInfo: getPushInfo,
328
+ endpoint: endpoint2,
329
+ entity,
330
+ actions,
331
+ onSynchronized
332
+ });
333
+ }
334
+ }
335
+ }
336
+ });
337
+ const pushEntities = Object.keys(this.pushAccessMap);
338
+ // push相关联的entity,在发生操作时,需要将operation推送到远端
339
+ const createOperTrigger = {
340
+ name: 'push oper to remote node',
341
+ entity: 'oper',
342
+ action: 'create',
343
+ when: 'commit',
344
+ strict: 'makeSure',
345
+ check: (operation) => {
346
+ const { data } = operation;
347
+ return pushEntities.includes(data.targetEntity);
348
+ },
349
+ fn: async ({ ids }, context) => {
350
+ (0, assert_1.default)(ids.length === 1);
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');
354
+ }
355
+ };
356
+ return createOperTrigger;
357
+ }
358
+ constructor(config, schema) {
359
+ this.config = config;
360
+ this.schema = schema;
361
+ }
362
+ /**
363
+ * 根据sync的定义,生成对应的 commit triggers
364
+ * @returns
365
+ */
366
+ getSyncTriggers() {
367
+ return [this.makeCreateOperTrigger()];
368
+ }
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
+ };
393
+ }
394
+ getSelfEndpoint() {
395
+ return {
396
+ name: this.config.self.endpoint || 'sync',
397
+ method: 'post',
398
+ params: ['entity', 'entityId'],
399
+ fn: async (context, params, headers, req, body) => {
400
+ // body中是传过来的oper数组信息
401
+ const { entity, entityId } = params;
402
+ const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
403
+ console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
404
+ const successIds = [];
405
+ let failed;
406
+ // todo 这里先缓存,不考虑本身同步相关信息的更新
407
+ if (!this.remotePullInfoMap[entity]) {
408
+ this.remotePullInfoMap[entity] = {};
409
+ }
410
+ if (!this.remotePullInfoMap[entity][entityId]) {
411
+ const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
412
+ const pullEntityDict = {};
413
+ if (pullEntities) {
414
+ pullEntities.forEach((def) => pullEntityDict[def.entity] = def);
415
+ }
416
+ this.remotePullInfoMap[entity][entityId] = {
417
+ pullInfo: await getPullInfo(context, {
418
+ selfId: meEntityId,
419
+ remoteEntityId: entityId,
420
+ }),
421
+ pullEntityDict,
422
+ };
423
+ }
424
+ const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId];
425
+ const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
426
+ (0, assert_1.default)(userId);
427
+ context.setCurrentUserId(userId);
428
+ if (cxtInfo) {
429
+ await context.initialize(cxtInfo);
430
+ }
431
+ // todo 解密
432
+ if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
433
+ const [maxHisOper] = await context.select('oper', {
434
+ data: {
435
+ id: 1,
436
+ bornAt: 1,
437
+ },
438
+ filter: {
439
+ operatorId: userId,
440
+ },
441
+ sorter: [
442
+ {
443
+ $attr: {
444
+ bornAt: 1,
445
+ },
446
+ $direction: 'desc',
447
+ },
448
+ ],
449
+ indexFrom: 0,
450
+ count: 1,
451
+ }, { dontCollect: true });
452
+ this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt || 0;
453
+ }
454
+ let maxBornAt = this.pullMaxBornAtMap[entityId];
455
+ const opers = body;
456
+ const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
457
+ const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
458
+ await Promise.all([
459
+ // 无法严格保证推送按bornAt,所以一旦还有outdatedOpers,检查其已经被apply
460
+ (async () => {
461
+ const ids = outdatedOpers.map(ele => ele.id);
462
+ if (ids.length > 0) {
463
+ const opersExisted = await context.select('oper', {
464
+ data: {
465
+ id: 1,
466
+ },
467
+ filter: {
468
+ id: {
469
+ $in: ids,
470
+ }
471
+ }
472
+ }, { dontCollect: true });
473
+ if (opersExisted.length < ids.length) {
474
+ const missed = (0, lodash_1.difference)(ids, opersExisted.map(ele => ele.id));
475
+ // todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
476
+ throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
477
+ }
478
+ successIds.push(...ids);
479
+ }
480
+ })(),
481
+ (async () => {
482
+ for (const freshOper of freshOpers) {
483
+ // freshOpers是按bornAt序产生的
484
+ const { id, targetEntity, action, data, bornAt, filter } = freshOper;
485
+ const ids = (0, filter_1.getRelevantIds)(filter);
486
+ (0, assert_1.default)(ids.length > 0);
487
+ try {
488
+ if (pullEntityDict && pullEntityDict[targetEntity]) {
489
+ const { process } = pullEntityDict[targetEntity];
490
+ if (process) {
491
+ await process(action, data, context);
492
+ }
493
+ }
494
+ const operation = {
495
+ id,
496
+ data,
497
+ action,
498
+ filter: {
499
+ id: ids.length === 1 ? ids[0] : {
500
+ $in: ids,
501
+ },
502
+ },
503
+ bornAt: bornAt,
504
+ };
505
+ await context.operate(targetEntity, operation, {});
506
+ successIds.push(id);
507
+ maxBornAt = bornAt;
508
+ }
509
+ catch (err) {
510
+ console.error(err);
511
+ console.error('sync时出错', entity, JSON.stringify(freshOper));
512
+ failed = {
513
+ id,
514
+ error: err.toString(),
515
+ };
516
+ break;
517
+ }
518
+ }
519
+ })()
520
+ ]);
521
+ this.pullMaxBornAtMap[entityId] = maxBornAt;
522
+ return {
523
+ successIds,
524
+ failed,
525
+ };
526
+ }
527
+ };
528
+ }
529
+ }
530
+ exports.default = Synchronizer;