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.
- package/lib/AppLoader.d.ts +43 -43
- package/lib/AppLoader.js +444 -437
- package/lib/ClusterAppLoader.d.ts +17 -17
- package/lib/DataSubscriber.d.ts +18 -18
- package/lib/DataSubscriber.js +158 -158
- package/lib/DbStore.d.ts +20 -20
- package/lib/Synchronizer.d.ts +46 -36
- package/lib/Synchronizer.js +530 -471
- package/lib/cluster/DataSubscriber.d.ts +23 -23
- package/lib/cluster/DataSubscriber.js +83 -83
- package/lib/types/Sync.d.ts +16 -16
- package/lib/types/Sync.js +5 -5
- package/package.json +42 -42
package/lib/Synchronizer.js
CHANGED
|
@@ -1,471 +1,530 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
channel.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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]:
|
|
43
|
-
[OAK_SYNC_HEADER_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
|
-
|
|
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
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
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;
|